Repository navigation
Keep a stream's pending messages receivable whatever moves the run cursor - #1017
Conversation
…rsor A run's message cursor is shared by signals, updates and every stream. Stream reads filtered on sequence > cursor, so a signal, an update or a receive on another stream hid older pending messages for good (#1011). peek, receive, pendingCount and hasPending now read every pending message of the stream in sequence order; consume_state already records what each stream consumed. The cursor and its history events are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # tests/Feature/V2/V2MessageStreamReplayTest.php
|
General interim review (Security unavailable), exact head Verified
H1 (blocking): runs already affected by #1011 break on upgrade, and rollback doesn't fix them.
M2 (docs)
|
General's hold on be836c2 (H1): with the cursor filter gone, a run whose earlier receive found nothing because the cursor hid a message would, on replay after upgrading, peek and consume that message at the old step. A branching workflow then failed with a history mismatch, and a straight-line one silently changed its result. A workflow-code receive with nothing recorded at its step now consumes only when history hasn't passed the step the code is at (read live, so a stream kept in a property still consumes at later steps). Otherwise it returns empty. This also closes the 2.20.1 note that an empty replayed receive reads the stream again. M2: api-stability.md and workflow-messages-architecture.md no longer describe stream reads as filtered by the run cursor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branching workflow from the #1011 review as a corpus fixture: turn one finds the inbox empty, a message arrives, and turn two must replay turn one's branch. On main the replay consumes the message at turn one's step and the run stays waiting with a history mismatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Addressed General's interim hold on
|
|
General interim review (Security unavailable): HOLD at exact head Verified
H1 (blocking, residual): a kept stream still consumes at a step history has passed.
Low (non-blocking)
|
General's re-review of a27bcc3 (residual H1): a stream kept in a property recorded its consumption at the step it was opened at, while the replay guard checked the live step. After an empty first turn, a later turn's consumption was found again at the first turn's step on replay, so straight-line runs returned it early and branching runs got stuck. Without an explicit step, a workflow-code receive now records and looks up its consumption at the live step, falling back to the opened step so rows earlier releases wrote still replay. Also: highestRecordedStep reuses loaded history and accepts numeric-string steps, and two stale 'after cursor' lines in workflow-messages-architecture.md are fixed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
General interim review (Security unavailable): HOLD at exact head Resolved since a27bcc3
N1 (blocking, regression since a27bcc3): an update handler can't receive once the run has passed its first step.
|
An update applied while the run waits at a later step runs its handler for the first time at the update's recorded step. The past-step guard treated that as a replay and returned nothing, so the message stayed pending. Exempt live update handlers from the guard. Query replay of update handler receives is tracked in #1024. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # tests/Feature/V2/V2EmbeddedReplayRegressionCorpusTest.php # tests/Feature/V2/V2MessageStreamReplayTest.php
|
N1 fixed in 2c46077 (merged with main at 56d535e).
|
# Conflicts: # CHANGELOG.md
|
|
2.20.3 release scope and acceptance plan, while the independent delta review holds #1017 at
#1024, #1021, #1022 and #1020 remain parked under the handoff's priority order. No tag or release action has been taken. |
|
Independent General source review: MERGE GO at exact head
Known query limitation #1024 remains separate. The 2.20.3 release PR must retain the explicit kept-stream downgrade warning: an older package looks up the opening step and can replay newly recorded later-step consumptions differently. Merge only the cleared SHA, verify the resulting tree, then prepare the scoped release PR for Security's exact release-head review and the planned installed-package acceptance. No tag go is granted here. Cleanup complete: the three task containers, their anonymous volumes, the task network, the review clone and its three owned worktrees are removed. Counterfactual edits were verified as exact recoverable GitHub blobs before deletion. Shared resources and other worktrees were preserved. |
|
Merged #1017 through GitHub with
The source fix is merged but remains unreleased. Preparing the scoped 2.20.3 release draft for Security, including the kept-stream downgrade warning and known limitation #1024. No tag or publication clearance has been granted. |



Fixes #1011.
Problem
MessageService::peekMessages,getUnconsumedCountandhasUnconsumedMessagesreturned only inbound messages withsequence > workflow_runs.message_cursor_position. That cursor is shared by signals, updates and every stream: signals and updates reserve theirmessage_sequencefrom the same per-instance sequence and advance the cursor when applied. So a signal, an update, or a receive on another stream moved the cursor past older pending messages of a stream. Those messages stayedpendingbut were never returned byinbox()->peek()/receive()/pendingCount()/hasPending()again.Fix
The three stream reads no longer filter by the run cursor. They return every
pendinginbound message of the stream for the run, in sequence order.MessageService::sendMessagecreatesworkflow_messagesrows. Commands only reserve sequences, so a stream message's state lives entirely inconsume_state.advanceCursoris still monotonic, and theMessageCursorAdvancedhistory events, the continue-as-new transfer and the redrive transfer are untouched.Tests (counterfactuals against
main6ce69f21)testASignalDoesNotHideAnOlderPendingStreamMessage:m1is sent before thepingsignal that wakes the receive, and the workflow receivesm1. Onmainit receivesnull.testAReceiveOnOneStreamDoesNotHideAnotherStreamsOlderMessage:b1is sent on streamb, thena1on streama. Consuminga1moves the cursor pastb1.peek('b'),getUnconsumedCount('b')andhasUnconsumedMessages('b')still seeb1. Onmainthey don't.Local results
Message*tests pass 160/160.Notes
CHANGELOG.mdoverlaps with Move a failed run's pending messages to its redrive successor #1016's Unreleased entry; I'll resolve whichever merges second.🤖 Generated with Claude Code